Skip to content

Conversation

erikhfj
Copy link
Member

@erikhfj erikhfj commented Sep 23, 2026 •

What

Brings up the whole Crate stack so the group can split the remaining work across M2–M4 without stepping on each other.

Community album catalog: search albums/artists, filter by genre and decade, sort the whole result set (including by community rating), page through large results, open an album, and persist ratings, reviews and a personal wantlist ("crate").

Why these choices

Written up properly in docs/architecture.md (10 ADRs). The short version:

Decision Instead of Because
PostgreSQL 18 + GraphQL Yoga MongoDB / Neo4j / Supabase The brief's hardest requirements are search and server-side sort/filter over the whole set. Postgres FTS (tsvector + GIN) and real ORDER BY/WHERE cover both. A BaaS also violates "egendefinert GraphQL backend" and "database set up directly on the VM"
Keyset (cursor) pagination OFFSET Flat cost on page 1 and page 5000; stable when rows are inserted between page loads. The brief asks for "udefinert stor datasamling"
Sort/filter in SQL rearrange the loaded page The brief is explicit that it must apply to the full result set. It also keeps paging consistent with the sort order
Facets in the same query separate facet request Keeps the filter panel in sync and avoids a second round-trip. Counts are computed with the counted dimension excluded, so selecting "Rock: 12" while "1990s" is active is still meaningful
Per-request DataLoaders ad-hoc joins in resolvers GraphQL's remaining weakness is server-side N+1 (called out in the lecture). Nested album → artist/tracks/genres/reviews is a constant number of queries
Simulated user (UUID in localStorage) real auth The lecture explicitly allows it. Removes passwords/sessions/GDPR while still meeting the actual requirement: user data persisted server-side
Apollo Client cache + URL as search state Redux + local state GraphQL has no HTTP caching, so the client cache is the caching strategy. URL state gives reload-persistence, back/forward and shareable links for free
Plain CSS, no UI framework MUI / Tailwind Smallest bundle (bærekraft), full control over focus rings and contrast (tilgjengelighet)

Layout

server/src/  schema.graphql · resolvers/ · search/ · loaders/ · db/
client/src/  api/ · components/ · hooks/ · test/
e2e/tests/   Playwright journeys
docs/        architecture.md · VM-setup.md · AI-USAGE.md
scripts/     one-shot VM bootstrap

Testing

  • 24 Vitest tests green (server 11, client 13): SQL builders (including a regression test for a parameter-binding bug), cursor round-trip, useDebounce, simulated-user identity, and component/interaction/a11y assertions (labels, aria-pressed, radio-group star rating, Enter + button submit).

  • Playwright E2E + axe-core scaffolded in e2e/ covering the full journey.

  • Live smoke suite run against the VM (documented in docs/AI-USAGE.md) - this caught three real bugs the unit tests missed, because unit tests only inspect SQL fragments and never execute them against Postgres:

    1. GenreFacet.genre serialised as genreId/genreName while the SDL declares genre: Genre!
    2. = ANY(${jsArray}::int[]) - drizzle expands a JS array into N scalar parameters, so the cast received a bare integer and the query aborted
    3. album(id:) selected no review aggregate, so reviewCount: Int! resolved to null

    All three fixed and covered.

Verified live on it2810-19.idi.ntnu.no

Check Result
GET /project2/ 200
POST /graphql (Apache proxy → :3001) 200
Case-insensitive search (ABBEY → Abbey Road) ✅
Genre + decade facets with counts ✅
Cursor pagination, hasNextPage, stable cursors ✅
RATING_DESC over AVG(review.rating) in SQL ✅
upsertReview / toggleCrate round-trip (persistence) ✅
Nested album → artist/tracks/genres/reviews in one request ✅

Infrastructure

  • PostgreSQL 18 on the VM (apt, no Docker), role t19, database crate, localhost only
  • systemd unit crate-api on port 3001
  • Apache ProxyPass /graphql → 127.0.0.1:3001
  • Node 24 + pnpm 10 via nvm
  • Full runbook: docs/VM-setup.md
  • CI/CD on the self-hosted runner: format → lint → typecheck → test → build → bundle-size budget → deploy to /var/www/html/project2 + systemctl restart crate-api + smoke check

Also in this PR

  • 22 labels (area / quality), 4 milestones matching the four underveisinnleveringer, 30 issues across M1–M4
  • AGENTS.md so agents and new members know the architecture rules
  • docs/AI-USAGE.md - AI usage log as the course requires

Closes

Closes #1, #2, #3, #4, #5, #6, #7, #8, #9, #10, #11, #12, #13, #14, #15, #16, #19, #27.

Partially addresses #17 (a11y), #18 (bærekraft), #20 (Playwright) - the code is in but the manual pass / live run / measured numbers are still open.

Follow-ups (already in the milestones)

#21 integration tests that execute SQL · #22 EXPLAIN ANALYZE at 100k+ · #23 GraphQL codegen · #24 cover images · #25 search history · #26 country facet · #28 README polish · #29 UI sketch · #30 manual a11y/usability check

How to review

pnpm install
pnpm format:check && pnpm lint && pnpm typecheck && pnpm test
pnpm dev        # API :3001 + client :5173

Or just open the live prototype: http://it2810-19.idi.ntnu.no/project2

Worth a close look: server/src/search/search.ts (WHERE / ORDER BY / cursor builders) and server/src/loaders/index.ts (N+1 defence) - those are where the graded requirements live.


AI-assisted: see docs/AI-USAGE.md. Every change here is going through peer review per CODEOWNERS.

…nt, tests, docs

Brings up the whole stack for Project 2 so the group can split the work.

server/
  GraphQL Yoga + Drizzle + PostgreSQL 16. Hand-written SDL in
  server/src/schema.graphql: searchAlbums returns a Relay-style connection
  with totalCount and facet counts alongside the page of edges.
  - Postgres FTS on a trigger-maintained search_vector (GIN) — single-term,
    case-insensitive search that stays fast as the catalog grows
  - keyset/cursor pagination in search/search.ts (never OFFSET)
  - sorting and filtering are SQL WHERE / ORDER BY over the whole result set
  - per-request DataLoaders so nested fields never degenerate into N+1
  - drizzle/0000_init.sql + idempotent migrate.ts
  - seed.ts (20 curated albums) and generate.ts (ALBUM_COUNT up to 1M)

client/
  React 19 + Vite + TS + Apollo Client. InMemoryCache typePolicies merge
  edges across fetchMore; query/filters/sort live in the URL so reload,
  back/forward and shared links all work.
  - search bar with debounce + explicit submit
  - facet panel with counts (categories that would match zero are hidden)
  - sort control, infinite scroll with a keyboard-operable Load more button
  - detail view (code-split) with tracklist, reviews, crate toggle
  - star rating as a radio group, skip link, aria-live result count
  - simulated user identity (UUID in localStorage) as the lecture allows

e2e/
  Playwright journey (search -> filter -> sort -> review -> reload) plus
  an axe-core scan.

docs/
  architecture.md (10 ADRs), VM-setup.md (runbook), AI-USAGE.md (log),
  AGENTS.md. scripts/ has the one-shot VM bootstrap.

CI/CD
  Self-hosted runner workflow: format, lint, typecheck, test, build,
  bundle-size budget, then deploy to Apache + restart crate-api.

Verified live on it2810-19.idi.ntnu.no: search, facets, cursor paging,
server-side rating sort, review/crate mutations, nested detail, proxy.

Co-Authored-By: opencode <noreply@opencode.ai>
@erikhfj erikhfj requested a review from a team as a code owner September 23, 2026 17:46
@erikhfj erikhfj self-assigned this Sep 23, 2026
@erikhfj
Copy link
Member Author

erikhfj commented Sep 25, 2026 •

Closing unmerged. Please do not merge this PR.

This is one commit with 67 files and +10 955 lines. That is not reviewable. More importantly, it collapses the entire project into a single commit, which is not how we want our git history to read. The group raised this and they are right.

What it was for: a prototype to validate the stack quickly (Postgres + GraphQL Yoga + React/Apollo) before we committed to it. It answered questions like "does Postgres FTS give us case-insensitive single-term search?" and "can we keep cursor pagination correct across every sort mode?". Those were worth answering early.

What it is not: the deliverable. A smaller PR history full of the same generated code would only fix the git cosmetics, not our understanding of the solution.

Where it went: the branch is kept as archive/scaffold-prototype as a reference: something to check against or get unstuck on, not a source to copy from. Each issue is implemented from the requirement by its owner.

What we are doing instead:

Each PR has one owner and one reviewer who is not the author. The AI usage log (docs/AI-USAGE.md) is being rewritten as a per-PR decision log covering what we asked for, what we took, and what we changed or rejected. That is how we document our own control over the process rather than just tool usage.

Tracked in #4. Happy to walk through the prototype if anyone wants to read ahead.

@erikhfj erikhfj closed this Sep 25, 2026
@erikhfj erikhfj deleted the feat/1-scaffold-crate-stack branch September 25, 2026 18:21
Sign in to join this conversation on GitHub.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Scaffold pnpm monorepo (client / server / e2e)
1 participant